#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <limits.h>

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "libsam3a/libsam3a.h"

char *argv0;

#include "arg.h"

////////////////////////////////////////////////////////////////////////////////
#define SSM_CHANNEL_MAX 24
#define SSM_MSG_MAX 4096
typedef struct Channel Channel;
struct Channel {
	int fdin;
	Sam3AConnection *ct;
	char name[SSM_CHANNEL_MAX]; /* channel name (normalized) */
	char inpath[PATH_MAX];      /* input path */
	char outpath[PATH_MAX];     /* output path */
	Channel *next;
};

static Channel * channel_add(const char *);
static Channel * channel_find(const char *);
static Channel * channel_join(const char *, Sam3AConnection *);
static void      channel_leave(Channel *);
static Channel * channel_new(const char *);
static void      channel_normalize_name(char *);
static void      channel_normalize_path(char *);
static int       channel_open(Channel *);
static void      channel_print(Channel *, const char *, int);
static int       channel_reopen(Channel *);
static void      channel_rm(Channel *);
static void      create_dirtree(const char *);
static void      create_filepath(char *, size_t, const char *, const char *, const char *);
static void      handle_channels_input(Sam3ASession *, Channel *);
static int       read_line(int, char *, size_t);

static Channel *channels = NULL;
static Channel *channelmaster = NULL;
static char *ssmpath = NULL;

////////////////////////////////////////////////////////////////////////////////
/* creates directories bottom-up, if necessary */
static void
create_dirtree(const char *dir)
{
	char tmp[PATH_MAX], *p;
	struct stat st;
	size_t len;

	strncpy(tmp, dir, sizeof(tmp));
	len = strlen(tmp);
	if (len > 0 && tmp[len - 1] == '/')
		tmp[len - 1] = '\0';

	if ((stat(tmp, &st) != -1) && S_ISDIR(st.st_mode))
		return; /* dir exists */

	for (p = tmp + 1; *p; p++) {
		if (*p != '/')
			continue;
		*p = '\0';
		mkdir(tmp, S_IRWXU);
		printf("made dir: %s\n", tmp);
		*p = '/';
	}
	mkdir(tmp, S_IRWXU);
	printf("made dir: %s\n", tmp);
}

static void
channel_normalize_path(char *s)
{
	for (; *s; s++) {
		if (isalpha((unsigned char)*s))
			*s = tolower((unsigned char)*s);
		else if (!isdigit((unsigned char)*s) && !strchr(".#&+!-", *s))
			*s = '_';
	}
}

static void
channel_normalize_name(char *s)
{
	char *p;

	while (*s == '&' || *s == '#')
		s++;
	for (p = s; *s; s++) {
		if (!strchr(" ,&#\x07", *s)) {
			*p = *s;
			p++;
		}
	}
	*p = '\0';
}

static void
create_filepath(char *filepath, size_t len, const char *path,
	const char *channel, const char *suffix)
{
	int r;

	if (channel[0]) {
		r = snprintf(filepath, len, "%s/%s", path, channel);
		if (r < 0 || (size_t)r >= len)
			goto error;
		create_dirtree(filepath);
		r = snprintf(filepath, len, "%s/%s/%s", path, channel, suffix);
		if (r < 0 || (size_t)r >= len)
			goto error;
	} else {
		r = snprintf(filepath, len, "%s/%s", path, suffix);
		if (r < 0 || (size_t)r >= len)
			goto error;
	}
	return;

error:
	fprintf(stderr, "%s: path to irc directory too long\n", argv0);
	exit(1);
}

static int
channel_open(Channel *c)
{
	int fd;
	struct stat st;

	/* make "in" fifo if it doesn't exist already. */
	if (lstat(c->inpath, &st) != -1) {
		if (!(st.st_mode & S_IFIFO))
			return -1;
	} else if (mkfifo(c->inpath, S_IRWXU)) {
		return -1;
	}
	c->fdin = -1;
	fd = open(c->inpath, O_RDONLY | O_NONBLOCK, 0);
	if (fd == -1)
		return -1;
	c->fdin = fd;

	return 0;
}

static int
channel_reopen(Channel *c)
{
	if (c->fdin > 2) {
		close(c->fdin);
		c->fdin = -1;
	}
	return channel_open(c);
}

static Channel *
channel_new(const char *name)
{
	Channel *c;
	char channelpath[PATH_MAX];

	strncpy(channelpath, name, sizeof(channelpath));
	//channel_normalize_path(channelpath);

	if (!(c = calloc(1, sizeof(Channel)))) {
		fprintf(stderr, "%s: calloc: %s\n", argv0, strerror(errno));
		exit(1);
	}

	strncpy(c->name, name, sizeof(c->name));
	//channel_normalize_name(c->name);

	create_filepath(c->inpath, sizeof(c->inpath), ssmpath,
		channelpath, "in");
	create_filepath(c->outpath, sizeof(c->outpath), ssmpath,
		channelpath, "out");
	return c;
}

static Channel *
channel_find(const char *name)
{
	Channel *c;
	char chan[SSM_CHANNEL_MAX];

	strncpy(chan, name, sizeof(chan));
	//channel_normalize_name(chan);
	for (c = channels; c; c = c->next) {
		if (!strcmp(chan, c->name))
			return c; /* already handled */
	}
	return NULL;
}

static Channel *
channel_add(const char *name)
{
	Channel *c;

	c = channel_new(name);
	if (channel_open(c) == -1) {
		fprintf(stderr, "%s: cannot create channel: %s: %s\n",
			argv0, name, strerror(errno));
		free(c);
		return NULL;
	}
	if (!channels) {
		channels = c;
	} else {
		c->next = channels;
		channels = c;
	}
	return c;
}

static Channel *
channel_join(const char *name, Sam3AConnection *ct)
{
	Channel *c;

	if (!(c = channel_find(name)))
		c = channel_add(name);
	c->ct = ct;
	return c;
}

static void
channel_rm(Channel *c)
{
	Channel *p;

	if (channels == c) {
		channels = channels->next;
	} else {
		for (p = channels; p && p->next != c; p = p->next)
			;
		if (p && p->next == c)
			p->next = c->next;
	}
	free(c);
}

static void
channel_leave(Channel *c)
{
	if (c->fdin > 2) {
		close(c->fdin);
		c->fdin = -1;
	}
	/* remove "in" file on leaving the channel */
	unlink(c->inpath);
	channel_rm(c);
}

static void
channel_print(Channel *c, const char* buf, int bufsize)
{
	FILE *fp = NULL;
	time_t t = time(NULL);

	if (!(fp = fopen(c->outpath, "a")))
		return;
	fprintf(fp, "%lu ", (unsigned long)t);
	for (int i = 0; i < bufsize; i++)
		fprintf(fp, "%c", buf[i]);
	fprintf(fp, "\n");
	fclose(fp);
}


////////////////////////////////////////////////////////////////////////////////
static void ccbError(Sam3AConnection *ct) {
  fprintf(stderr,
          "\n===============================\nCONNECTION_ERROR: "
          "[%s]\n===============================\n",
          ct->error);
}

static void ccbDisconnected(Sam3AConnection *ct) {
  fprintf(stderr, "\n===============================\nCONNECTION_"
                  "DISCONNECTED\n===============================\n");

  char name[SSM_CHANNEL_MAX];
  memcpy(name, ct->destkey, SSM_CHANNEL_MAX - 1);
  name[SSM_CHANNEL_MAX - 1] = '\0';

  Channel* c = channel_find(name);
  channel_leave(c);
}

static void ccbConnected(Sam3AConnection *ct) {
  fprintf(stderr, "\n===============================\nCONNECTION_CONNECTED\n==="
                  "============================\n");
  char name[SSM_CHANNEL_MAX];
  memcpy(name, ct->destkey, SSM_CHANNEL_MAX - 1);
  name[SSM_CHANNEL_MAX - 1] = '\0';

  channel_join(name, ct);
}

static void ccbAccepted(Sam3AConnection *ct) {
  fprintf(stderr, "\n===============================\nCONNECTION_ACCEPTED\n===="
                  "===========================\n");
  fprintf(stderr, "FROM: %s\n===============================\n", ct->destkey);

  char name[SSM_CHANNEL_MAX];
  memcpy(name, ct->destkey, SSM_CHANNEL_MAX - 1);
  name[SSM_CHANNEL_MAX - 1] = '\0';

  channel_join(name, ct);
}

static void ccbSent(Sam3AConnection *ct) {
  fprintf(stderr, "\n===============================\nCONNECTION_WANTBYTES\n==="
                  "============================\n");
}

static void ccbRead(Sam3AConnection *ct, const void *buf, int bufsize) {
  fprintf(stderr,
          "\n===============================\nCONNECTION_GOTBYTES "
          "(%d)\n===============================\n",
          bufsize);

  Channel *c;

  for (c = channels; c; c = c->next)
	if (c->ct == ct)
		break;

  if (!c)
	fprintf(stderr, "Channel not found\n");

  channel_print(c, buf, bufsize);
}

static void ccbDestroy(Sam3AConnection *ct) {
  fprintf(stderr, "\n===============================\nCONNECTION_DESTROY\n====="
                  "==========================\n");
}

static const Sam3AConnectionCallbacks ccb = {
    .cbError = ccbError,
    .cbDisconnected = ccbDisconnected,
    .cbConnected = ccbConnected,
    .cbAccepted = ccbAccepted,
    .cbSent = ccbSent,
    .cbRead = ccbRead,
    .cbDestroy = ccbDestroy,
};

////////////////////////////////////////////////////////////////////////////////
static void scbError(Sam3ASession *ses) {
  fprintf(stderr,
          "\n===============================\nSESION_ERROR: "
          "[%s]\n===============================\n",
          ses->error);
}

static void scbCreated(Sam3ASession *ses) {
  Sam3AConnection *conn;

  fprintf(stderr, "\n===============================\nSESION_CREATED\n");
  fprintf(stderr, "\rPRIV: %s\n", ses->privkey);
  fprintf(stderr, "\nPUB: %s\n===============================\n", ses->pubkey);

  channelmaster = channel_add(""); /* master channel */

  if ((conn = sam3aStreamAccept(ses, &ccb)) == NULL) {
	fprintf(stderr, "ERROR: CAN'T CREATE CONNECTION!\n");
	sam3aCancelSession(ses);
	return;
  }		        
}

static void scbDisconnected(Sam3ASession *ses) {
  fprintf(stderr, "\n===============================\nSESION_DISCONNECTED\n===="
                  "===========================\n");
}

static void scbDGramRead(Sam3ASession *ses, const void *buf, int bufsize) {
  fprintf(stderr, "\n===============================\nSESION_DATAGRAM_READ\n==="
                  "============================\n");
}

static void scbDestroy(Sam3ASession *ses) {
  fprintf(stderr, "\n===============================\nSESION_DESTROYED\n======="
                  "========================\n");
}

static const Sam3ASessionCallbacks scb = {
    .cbError = scbError,
    .cbCreated = scbCreated,
    .cbDisconnected = scbDisconnected,
    .cbDatagramRead = scbDGramRead,
    .cbDestroy = scbDestroy,
};

////////////////////////////////////////////////////////////////////////////////
static int
read_line(int fd, char *buf, size_t bufsiz)
{
	size_t i = 0;
	char c = '\0';

	do {
		if (read(fd, &c, sizeof(char)) != sizeof(char))
			return -1;
		buf[i++] = c;
	} while (c != '\n' && i < bufsiz);
	buf[i - 1] = '\0'; /* eliminates '\n' */
	return 0;
}

static void
handle_channels_input(Sam3ASession *ses, Channel *c)
{
	char buf[SSM_MSG_MAX];

	if (read_line(c->fdin, buf, sizeof(buf)) == -1) {
		if (channel_reopen(c) == -1)
			channel_rm(c);
		return;
	}

	if (c == channelmaster) {
		if (!sam3aIsValidPubKey(buf)) {
			fprintf(stderr, "Invalid public key input in master channel\n");
			return;
		}
		if (sam3aStreamConnect(ses, &ccb, buf) == NULL) {
			fprintf(stderr, "ERROR: CAN'T CREATE CONNECTION!\n");
			sam3aCancelSession(ses);
			return;
		}
	} else {
		if (sam3aIsActiveConnection(c->ct)) {
			channel_print(c, buf, strlen(buf));
			sam3aSend(c->ct, buf, strlen(buf));
		} else {
			fprintf(stderr, "Connection of channel %s is not active", c->name);
			return;
		}
	}
}

#define HOST SAM3A_HOST_DEFAULT

int main(int argc, char *argv[]) {
  ssmpath = getenv("SSM_DIR");
  Sam3ASession ses;
  Sam3AConnection *conn;
  //
  libsam3a_debug = 0;
  //
  if (sam3aCreateSession(&ses, &scb, HOST, SAM3A_PORT_DEFAULT,
                         SAM3A_DESTINATION_TRANSIENT,
                         SAM3A_SESSION_STREAM) < 0) {
    fprintf(stderr, "FATAL: can't create main session!\n");
    return 1;
  }

  //
  while (sam3aIsActiveSession(&ses)) {
    fd_set rds, wrs;
    Channel *c;
    int res, maxfd = 0;
    struct timeval to;
    //
    FD_ZERO(&rds);
    FD_ZERO(&wrs);
  if ((conn = sam3aStreamAccept(&ses, &ccb)) == NULL) {
	fprintf(stderr, "ERROR: CAN'T CREATE CONNECTION: %s\n", ses.error);
	sam3aCancelSession(&ses);
	return -1;
  }		        
    if (sam3aIsActiveSession(&ses) &&
        (maxfd = sam3aAddSessionToFDS(&ses, -1, &rds, &wrs)) < 0)
      break;
    for (c = channels; c; c = c->next) {
      if (c->fdin > maxfd) {
        maxfd = c->fdin;
      }
      FD_SET(c->fdin, &rds);
    }
    sam3ams2timeval(&to, 1000);
    res = select(maxfd + 1, &rds, &wrs, NULL, &to);
    if (res < 0) {
      if (errno == EINTR)
        continue;
      fprintf(stderr, "FATAL: select() error!\n");
      break;
    }
    if (res == 0) {
      fprintf(stdout, ".");
      fflush(stdout);
    } else {
      if (sam3aIsActiveSession(&ses))
        sam3aProcessSessionIO(&ses, &rds, &wrs);
      for (c = channels; c; c = c->next) {
        if (FD_ISSET(c->fdin, &rds)) {
		handle_channels_input(&ses, c);
        }
      }
    }
  }
  //
  sam3aCloseSession(&ses);
  //
  return 0;
}
